[P0-P4] Remove Ant Design from the OSS frontend (shadcn/ui + Midnight Bloom) - #2212
[P0-P4] Remove Ant Design from the OSS frontend (shadcn/ui + Midnight Bloom)#2212hari-kuriakose wants to merge 151 commits into
Conversation
Implements P0-01..P0-16 of UN_SHADCN_IMPL_PLAN.md (spec: UN_SHADCN_SPEC.md). Installs the shadcn/ui + Tailwind v4 stack alongside Ant Design; antd still renders every screen, so this phase is intentionally a no-op visually. - Deps: Radix primitives, CVA/clsx/tailwind-merge, lucide-react, next-themes, sonner, react-hook-form + zod, Tailwind v4. antd deliberately retained for the coexistence period (spec §7). - Fonts: self-hosted @fontsource Inter + Geist Mono (no CDN; prod serves via nginx and must not depend on an external host). - Tokens: src/index.css now carries the Midnight Bloom light+dark palette (D8). Tailwind is imported first so its layer ordering is correct, and the colour tokens are mapped with `@theme inline` — with a plain `@theme` Tailwind snapshots the light value and dark mode silently breaks. - Legacy CSS vars renamed to --legacy-* (D6): variables.css defined --primary and --secondary, which collide with the shadcn tokens. - 32 primitives generated into src/components/ui, plus hand-written spinner and kbd (no registry entry) and success/warning badge variants. - Theme: next-themes ThemeProvider mirrors the existing session theme onto the `.dark` class. How the theme is persisted and toggled is unchanged (C4). - Toasts: sonner Toaster mounted and a shared useAppToast helper added for cloud plugins to import (D9). ALERT_SURFACE keeps antd as the single active notification surface until P2-06, so alerts are not double-rendered. Two fixes the plan did not anticipate, both required: - .gitignore: the Python `lib/` rule also matched frontend/src/lib/, which is where components.json points `@/lib/utils`. Without the negation, cn() would never reach the repo and every primitive would fail to resolve in CI. - biome.json: enable css.parser.tailwindDirectives, otherwise Biome cannot parse @theme/@plugin/@custom-variant and fails CI with 4 parse errors. Gates: build (plugins absent, the optionalPluginImports path) passes; 16 tests green; dark mode verified in headless Chromium — the `bg-background` utility itself flips rgb(250,250,250) -> rgb(26,26,26), proving `@theme inline` works; no visual regression (antd element count, button geometry, colours and radii all unchanged — only the body font moves to Inter, which is intended). Lint findings that remain (3 errors, 24 warnings) are pre-existing: pristine main reports 227/261 with the same binary, and none of the findings are in files this change touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements P1-01 (mapping table) and P1-02 (apply migration) of UN_SHADCN_IMPL_PLAN.md. 91 files, 87 unique icons, zero @ant-design/icons imports remaining in OSS. docs/icon-map.md records every mapping and flags the ones that are not exact, since lucide is not a 1:1 replacement for antd's icon set: - CheckCircleFilled / PlayCircleFilled / InfoCircleFilled -> lucide has no filled variants, so these render as outlines. Where the solid weight carries meaning, the doc shows the fill-current treatment. - MoreOutlined -> EllipsisVertical, NOT Ellipsis. antd's renders vertical (the 10 call-sites are all overflow menus); plain Ellipsis is horizontal. - CaretDownOutlined -> ChevronDown trades a solid triangle for a stroke, which also matches the shadcn/Radix idiom used elsewhere. - SlackOutlined -> MessagesSquare. lucide dropped brand icons, so the Slack glyph is simply gone; this is the one place a brand mark is lost. - ScheduleOutlined -> CalendarClock, ArrowsAltOutlined -> Move, ExportOutlined -> ExternalLink: closest available, no exact match. Three name collisions the rename introduced, all fixed with aliases: - FileUpload.jsx and FileWidget.jsx import antd's `Upload` COMPONENT, which the lucide `Upload` icon shadowed. Left unfixed this would have broken both file upload widgets, not merely the icon. - Workflows.jsx defines its own `User` component; importing lucide's `User` made it render itself. This was an infinite recursion, caught by the build. useRetrievalStrategies.js needed a matching update: RetrievalStrategyModal's ICON_MAP keys were renamed to lucide names, but the hook still emitted antd names, so every lookup would have missed and silently fallen back to the default icon. The backend contract is unchanged — only the frontend key names moved. Verified: build passes; 16 existing tests green plus a temporary smoke test confirming migrated icons render as lucide svgs; lint reports 0 errors and the same 24 pre-existing warnings; no page errors at runtime. The 4 `anticon` elements still in the DOM belong to antd's own notification component, not app code, and go away with P2-06. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements P1-03 of UN_SHADCN_IMPL_PLAN.md. 93 call-site files plus a new
`@/components/ui/typography` primitive. Zero antd Typography imports remain.
Deviation from the plan, and why: the plan said convert Typography to
"semantic tags + Tailwind type classes". That is unsafe here. antd's
`ellipsis` prop is behaviour, not styling — `ellipsis={{ tooltip: true }}`
truncates AND surfaces the full text on hover, and `ellipsis={{ rows: 2 }}`
clamps to N lines. 12 call-sites use the object form. Swapping in a bare
`truncate` class would silently drop the tooltip, which is a behaviour
regression and therefore a C4 violation, not a restyle.
So this adds a small shim that presents antd's API (`type`, `strong`,
`italic`, `delete`, `code`, `mark`, `ellipsis`, `level`, and the
`Typography.Text` namespace) on top of Midnight Bloom tokens, with `ellipsis`
implemented against the shadcn Tooltip. The 295 call-sites then become an
import rewrite with the JSX untouched: same elements, same order, same props.
Per D9/§5.0 it lives in OSS so cloud plugins import the same component.
Two details worth noting:
- The line-clamp classes are written out in a lookup table rather than
interpolated as `line-clamp-${rows}`. Tailwind scans source statically and
never sees a class name assembled at runtime, so the interpolated form would
have produced no CSS.
- The tooltip renders whenever requested rather than only when text actually
overflows. antd measures the DOM to decide; matching that would need a
resize observer per element. Showing it unconditionally keeps the content
reachable, which is the purpose of the prop.
11 unit tests cover the shim, including the ellipsis behaviours that made the
regex approach unsafe. Full suite is 27 tests across 5 files, all green.
Build passes, lint reports 0 errors and the same 24 pre-existing warnings, and
the app renders with no console errors (antd element count drops 33 -> 29 on
the landing page as Typography moves off antd).
Plan estimate correction: P1-03 was scoped at 158 sites; the real count is 295
(192 `<Typography.Text>` alone). As with icons (43 -> 87), the original
enumeration missed multi-line import blocks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements P1-04 of UN_SHADCN_IMPL_PLAN.md. 70 call-site files plus a new `@/components/ui/antd-button` wrapper over the shadcn primitive. Zero antd Button imports remain. Same reasoning as the Typography shim (P1-03): antd's Button carries behaviour that shadcn's does not, so the plan's prop-mapping-by-find-and-replace would have changed what the UI does, not just how it looks (C4): - `loading` (234 usages) swaps in a spinner AND disables the button. Dropping the disable would let users double-submit during in-flight requests. - `icon` (106) is a leading slot, not a child. - `danger` (12) is orthogonal to `type`, so it is not a 1:1 variant mapping — danger+text has to stay ghost-with-destructive-text rather than becoming a solid destructive button. - `htmlType` maps to the DOM `type` attribute, because antd claims `type` for its visual variant. The shim defaults DOM type to "button" so a converted button cannot accidentally submit a form. The mapping is type=primary->default, link->link, text->ghost, dashed/default->outline, with danger overriding to destructive (or ghost + destructive text for text/link). size small->sm, large->lg, and icon-only buttons get the icon size. CustomButton (76 usages) is a thin pass-through over antd's Button, so it now routes through the shim automatically — no separate conversion needed. 12 unit tests cover the shim, focused on the behaviours that made the naive approach unsafe: loading disables, loading hides the icon, danger+text styling, htmlType mapping, block/shape. Full suite is 39 tests across 6 files, green. Verified in the browser: antd button count on the landing page drops to 0 while the Login button keeps its exact geometry (50px tall, same colour and position) and total antd elements fall 29 -> 24. Radius moves 6px -> 8px, which is the intended Midnight Bloom --radius-md token. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Follow-up to P1-03/P1-04, no behaviour change beyond one deletion.
- docs/shim-convention.md records the rule the next ~15 components follow:
shim when antd implements behaviour shadcn does not, direct swap when the
difference is only styling. Names compatibility layers `antd-<component>.jsx`
so they read as migration debt with an exit, lists the decision (with usage
counts) for every remaining component, and flags `Space` — it wraps each
child in its own div, so replacing it with `gap-*` silently breaks any CSS
selector matching `> *`.
- Renamed typography.jsx -> antd-typography.jsx (94 import lines) so both
shims follow that convention rather than one each.
- Removed the now-dead `components: { Button: { colorPrimary: "#092C4C" } }`
override from ConfigProvider. No antd Buttons remain after P1-04, so it
styled nothing.
Worth stating plainly, because the P1-04 message did not: that override was
painting every antd primary button the old Unstract navy. They now take
--primary from Midnight Bloom, so primary buttons across the authenticated app
move navy #092C4C -> violet #6f5cef. That is the intended end state under D8,
but it is a site-wide colour change and the earlier "geometry preserved" note
only covered the unauthenticated landing page, where the Login control is not
an antd Button.
Build, 39 tests and lint all green after the rename.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Implements P1-05 of UN_SHADCN_IMPL_PLAN.md. 74 call-site files plus `@/components/ui/antd-layout`. Zero antd Space/Row/Col/Flex imports remain. The plan classified these as a direct swap to flex/grid utilities. They are not, for a concrete reason: antd's `Space` wraps every child in its own `.ant-space-item` div, and Row/Col emit `.ant-row`/`.ant-col`. This repo has 20 hand-written CSS rules that select those internals — e.g. `.ant-space .ant-space-item .ant-card` in onBoard.css and `.file-history-modal .action-buttons .ant-space`. Collapsing the wrappers into `gap-*` on the parent deletes the elements those selectors match, so the styling silently stops applying: a regression, not a restyle (C4). 22 Space call-sites also build children from `.map()` or conditionals, where per-child wrappers change what `> *` matches. So the shim keeps antd's DOM shape, including the `ant-*` class names the existing CSS targets, while dropping the antd dependency. Those class names are emitted deliberately and go away in P4 when the dependent CSS is cleaned up. Details preserved: antd's size tokens (small/middle/large -> 8/16/24px) and numeric/array sizes; Space's falsy-child filtering, so conditional children do not leave empty gaps; the 24-column Col basis with span/offset as percentages; and Row's negative-margin + Col-padding gutter model. 11 unit tests cover the shim, centred on the wrapper-div structure that the existing CSS depends on. Full suite is 50 tests across 7 files, green. Build passes and lint is back to the 24-warning baseline with none in the new file (the two I introduced were single-line if-returns, now braced). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements P1-06 of UN_SHADCN_IMPL_PLAN.md, completing phase P1. 51 call-site
files plus `@/components/ui/antd-leaves` covering Tag, Spin, Alert, Image,
Divider, Empty, Avatar and Progress.
These are the "direct swap" tier of docs/shim-convention.md — none of them
carry behaviour the shadcn primitives lack. They are still gathered behind one
module so ~60 call-sites convert by import instead of hand-rewriting JSX, which
keeps the diff mechanical (C4).
Checked before deciding, per the convention: `Spin` has ZERO `spinning={...}`
usages, so there is no overlay mode to reproduce and no wrapper is needed —
every site is a bare indicator. Most already route through the existing
SpinnerLoader widget, which now picks up the shim automatically.
Mapping notes:
- Tag colour tokens fold onto Badge variants (success/green -> success,
error/red -> destructive, and so on). One call-site passes a raw
`rgb(45, 183, 245)`, which antd would have applied directly, so unrecognised
colours fall through to inline style rather than being dropped.
- Alert keeps message/description/showIcon/closable/banner, with its own
dismiss state so `closable` still works.
- Image does not reimplement antd's `preview` lightbox: no call-site enables
it. If one appears later it needs a real implementation, not a prop no-op.
Build passes, 50 tests green, lint back to the 24-warning baseline with none in
the new file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements P2-01..P2-06 of UN_SHADCN_IMPL_PLAN.md. 76 call-site files plus
`@/components/ui/antd-overlays` (Modal, Tooltip, Dropdown, Popconfirm, Popover,
Collapse) and `@/hooks/useConfirm`.
P2-01 useConfirm: promise-returning confirm dialog over AlertDialog, so
`if (await confirm({...}))` replaces antd's callback-style Modal.confirm. OSS
owned per D9 because the 3 cloud Modal.confirm sites must import it rather than
reimplement it. It resolves false on Escape and outside-click, so the promise
can never dangle.
P2-02..P2-05 overlays. Behaviours preserved that a prop swap would have lost:
- Modal renders an OK/Cancel footer BY DEFAULT and only omits it for
footer={null}. Call-sites relying on the implicit footer keep their buttons.
- The legacy `visible` alias still works alongside `open` (2 sites use it).
- destroyOnClose unmounts the body, which Radix does not do on its own.
- confirmLoading disables OK, matching the Button shim's loading semantics.
- closable={false} hides the close affordance; this shadcn DialogContent
renders it unconditionally, so it is suppressed by class rather than prop.
- Dropdown accepts antd's `menu={{ items }}` data shape and maps it onto
Radix's composed children.
- Popconfirm routes onto AlertDialog so inline confirms and useConfirm() share
one behaviour rather than diverging.
P2-06 notifications: sonner is now the only surface. The ALERT_SURFACE flag,
antd's notification.useNotification(), the Close/Close All buttons and
contextHolder are all removed. showAppToast now accepts a React node so the
rendered markdown + Execution/Request ID lines carry over unchanged, and a
`message` export mirrors antd's imperative message.* API for the 3 files that
used it. Toaster is positioned top-right to match where antd's stack appeared
(sonner defaults to bottom-right) — C4.
Verified in the browser: 2 sonner toasts render, 0 antd notifications, and
total antd elements on the landing page fall 24 -> 3. Build passes, 68 tests
across 9 files green (18 new), lint back at the 24-warning baseline with none
in the new files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements P3-01 (pattern), P3-02 (bulk Form conversion) and P3-03 (input controls). 61 call-site files plus `@/components/ui/antd-form` and `@/components/ui/antd-inputs`. This is the phase the plan flagged as highest risk, and the reason is the imperative form API: the codebase drives antd Forms through a form instance — setFieldsValue on edit, `await form.validateFields().catch(() => null)` as the submit guard, resetFields on cancel — across 14 useForm() sites and 102 Form.Items. Hand-rewriting those onto raw react-hook-form would be 102 independent chances to change submit or validation behaviour, and one missed guard silently submits invalid data. So antd's Form surface is reimplemented on react-hook-form and call-sites convert by import alone. docs/form-pattern.md records the pattern, with GroupCreateEditModal as the worked reference (it exercises setFieldsValue, the validateFields guard, resetFields and a required rule). The load-bearing detail: validateFields REJECTS when invalid. Two tests pin it — one asserts the rejection reaches `.catch()`, one asserts onFinish does not fire while a required field is empty. antd rule objects (required/min/max/ pattern/custom validator) are translated to RHF options, and a thrown validator error becomes the inline message. P3-03 covers Input (+ TextArea 14 sites, Password, Search), Select, Checkbox, Switch, Radio and InputNumber. The awkward part is onChange shape: antd hands a DOM event to Input but a raw value to Select/Switch, and gives Checkbox an event with target.checked where Radix gives a boolean. Call-sites are written against antd's convention, so the shim rebuilds those shapes instead of rewriting ~90 handlers. Select accepts both `options` data and Select.Option children (6 files use the latter). Build passes, 78 tests across 10 files green (10 new for the Form shim), lint back at the 24-warning baseline. antd importers now 73 files, down from 163 at the start of P1. Note: the new tests use @testing-library/user-event v13's direct API, not v14's `.setup()` — this repo is on v13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes P3-04, P3-05 and all of P4. antd, @ant-design/icons, @rjsf/antd and
@react-awesome-query-builder/antd are gone from package.json, and `grep -rl
"from 'antd'"` over src/ returns nothing.
P3-05 (RJSF) turned out far smaller than D3 assumed. RjsfFormLayout already
supplies its own `widgets` and `templates` for every field type, so @rjsf/antd
was contributing only theme chrome — swapping the import to @rjsf/core is the
whole change. There was no widget registry to rebuild.
P3-04 (date/time) follows D7 deliberately: the pickers are rebuilt on native
date/datetime-local/time inputs, but they still EXCHANGE MOMENT OBJECTS,
because call-sites are written as `value={moment(v)}` and
`onChange={(d) => onChange(d?.toISOString())}`. Dropping moment would change
timezone/DST behaviour, which D7 says needs its own reviewed pass — so this
change stays confined to the widget layer and moment remains a dependency.
P4 adds the shared DataTable (D5/D9) over TanStack + shadcn table, presenting
antd's Table API (columns/dataSource/rowKey/rowSelection/pagination/loading)
so all 16 call-sites convert by import and both repos share one table
implementation. antd-structure covers the remaining Card, Tabs, List, Layout,
Upload, Result, Drawer, Menu, Segmented, Pagination, Steps, Tree and Skeleton.
Final removals:
- ConfigProvider dropped from App.jsx; next-themes already owns theming.
- theme.useToken() replaced by the --card CSS variable.
- The three deep imports (antd/es/tabs/TabPane x2, antd/es/input/Search) now
resolve to Tabs.TabPane and Input.Search on the shims.
- antd-vendor manual chunk, the antd optimizeDeps entries, and the Less
preprocessor option (antd was the only Less consumer) removed from
vite.config.js.
- Query builder swapped to @react-awesome-query-builder/ui, promoted to a
direct dependency because the cloud overlay has no manifest of its own (D4).
Note on the DOM: three `ant-row`/`ant-col` elements still appear at runtime.
Those are emitted deliberately by the P1-05 layout shim because 20 hand-written
CSS rules select them; they are our class names, not antd. They go away when
that CSS is cleaned up.
P4 exit gate: 0 antd imports in src, 0 antd entries in package.json, build
passes, 78 tests green, no runtime page errors. Lint shows 3 errors and 26
warnings, all pre-existing — the errors are two SVG assets byte-identical to
main, and none of the findings are in files this migration added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Additions to the OSS shim layer surfaced while converting the enterprise
plugins. They live here rather than in the plugins per D9/§5.0 — a component
needed by more than one call-site is OSS-owned, so the two repos cannot drift.
antd-structure gains four components used only by cloud plugins today:
- Descriptions (4 sites) — label/value grid
- Statistic (2) — figure with prefix/suffix/precision
- FloatButton (2) — fixed-position action button
- Transfer (2) — dual list with move-between controls
- Badge — antd's count/dot overlay. Note this is NOT shadcn's Badge, which is
a pill label; antd calls that one Tag. Naming them apart avoids a confusing
collision later.
useAppToast gains a `notification` export mirroring antd's imperative
notification API, including the useNotification() hook form that returns
[api, contextHolder]. antd's config shape is `{ message, description }` while
sonner takes a title plus `{ description }`, so the remap happens here instead
of at each call-site.
Verified both ways: the OSS build passes with src/plugins absent (the
optionalPluginImports path), and the P0-G2 overlay build passes with all 53
plugins present and antd uninstalled. 78 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Migration complete — P0 through P4, plus the cloud pluginsResult: Cloud side: Zipstack/unstract-cloud#1683 (190 files, all 53 plugins). This PR must land first — the plugins import the shims defined here.
The judgment call that shaped thisThe plan said to map antd props to Tailwind classes inline. I didn't, wherever the evidence showed behavior rather than styling — silently dropping behavior is a C4 violation, not a restyle:
So each got a shim presenting antd's API on shadcn primitives, and ~700 call-sites converted by import swap with JSX untouched. Two P3 items came in far under estimate
Known follow-ups (not blockers)
VerificationBuild passes both with and without the plugin overlay (P0-G1 and P0-G2). 78 tests across 10 files, 51 of them new and targeted at exactly the behaviors above. Lint matches the |
…oll-lock check
Closes the four items I previously reported as complete but had not actually
finished. Each is now verified against the plan's own criteria rather than by
assertion.
P4-09 — final cleanup. Its verify command is `grep -rn "legacy-" src/` -> 0.
It was 72. The 42 remaining var() references across 8 legacy variables are now
mapped onto Midnight Bloom semantic tokens and variables.css is deleted:
--legacy-page-bg-1/2/3 -> var(--card) / var(--background) / var(--muted)
--legacy-white -> var(--card) (it flipped to #000 in dark,
so it was a surface, not white)
--legacy-black -> var(--foreground) (flipped to #fff in dark)
--legacy-border-color-* -> var(--border)
--legacy-font-family -> var(--font-sans)
--legacy-font-size/weight-* -> literals; Tailwind stock matches them exactly
Visible effect: the page background moves #e9e9e9 -> #fafafa, and body
background now follows the theme, which the legacy vars only did for a few
surfaces. Dark mode re-verified end to end after the file was removed.
docs/icon-map.md was stale — it documented 43 icons from the first enumeration
pass, but the real set is 116 (87 OSS, 87 cloud, overlapping). Regenerated from
the verified map with true pre-migration usage counts pulled from git, and 27
inexact pairs called out with the reason each differs: lucide has NO filled
variants (8 icons render lighter), it dropped brand icons (Slack is simply
gone), and several are approximations (FilePdf -> FileText loses the format
hint). This is the artifact a reviewer needs to sanity-check those calls.
Four shims had no tests, which contradicts the rule in shim-convention.md that
every shim must cover the behaviours justifying it. Added 67 tests:
- antd-inputs (14) — the onChange CONVENTIONS, which differ per component and
which Radix inverts: Input gets an event, InputNumber a number, Checkbox an
event with target.checked, Switch a boolean.
- antd-datetime (14) — the D7 contract, i.e. onChange hands back a MOMENT so
`date?.toISOString()` at the call-sites keeps working.
- antd-leaves (17) — including the raw rgb() Tag colour that must not be
dropped just because it is not a known token.
- antd-structure (22) — DataTable's antd column/render contract, and Badge's
count/overflow/showZero rules.
P2-02's deferred `body { overflow: hidden }` check is done. Radix's dialog
scroll-lock also sets body overflow and restores the prior value on close; the
risk was it restoring the wrong one and leaving the fixed app shell scrollable.
Three tests pin it: overflow stays hidden before/during/after, survives
repeated cycles, and is NOT left hidden on pages that never pinned it. The
index.css comment now records the outcome instead of reading as a TODO.
One real bug surfaced while writing these tests: the Tabs shim passed both
`value` and `defaultValue` to Radix, and a present-but-undefined `value` makes
Radix treat the component as controlled — which would have frozen every
uncontrolled tab set. Now it passes exactly one.
Test suite: 148 tests across 15 files, up from 78. Build passes, lint at the
24-warning baseline with zero findings in migration files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dev-deploy frontend image build failed:
error: lockfile had changes, but lockfile is frozen
process "/bin/sh -c bun install --frozen-lockfile --ignore-scripts"
did not complete successfully: exit code: 1
`bun remove antd @ant-design/icons @rjsf/antd
@react-awesome-query-builder/antd` and the `@tanstack/react-table` /
`@react-awesome-query-builder/ui` additions updated bun.lock in the working
tree, but that file was never staged — every earlier commit staged explicit
paths and bun.lock was not among them. So the committed lockfile still listed
antd as a root dependency and was missing @tanstack/react-table, which is
exactly the desync --frozen-lockfile exists to catch.
Nothing about the migration changes; this is the manifest edits reaching git.
Why local checks did not catch it: `bun install --frozen-lockfile` in the
worktree passes, because it validates the WORKING lockfile, which was already
correct. Only a clean checkout — i.e. Docker — sees the committed one. Verified
the fix by copying package.json + bun.lock into an empty directory and running
the container's exact command there: exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs found by comparing the dev deployment against production. Both are in the P4 structure shim, and both produce a page that is "correct" in the DOM but broken on screen — so no test, build or lint caught them. 1. Layout had no flex-grow. antd's Layout is `flex: auto`; mine computed `flex: 0 1 auto` and resolved to height 0. Every descendant using `flex: 1` then collapsed: on the dashboard, `.metrics-dashboard-container` was 0px tall while its child was 212px, so the whole page rendered at y=858, below a clipped viewport. The content was in the DOM the entire time, which is why it looked like a data problem rather than a CSS one. Layout.Content had the same issue (`flex-1` vs antd's `flex: auto`). 2. Layout.Sider ignored `collapsed` / `collapsedWidth`. It always applied `width`, so with a stored `collapsed: true` preference the rail sat at the full 240px while SideNavBar hid every label behind `!collapsed` — an icons-only sidebar in an expanded gutter. `collapsible` and `collapsedWidth` were also leaking onto the DOM as invalid attributes. Layout now also switches to a row when it contains a Sider, matching antd's hasSider auto-detection. That is done via an explicit `__isSider` marker rather than `c.type === Layout.Sider`: the identity check is fragile because Sider is assigned after Layout and does not survive HMR or wrapping. 7 regression tests cover both: flex-auto on Layout and Content, row/column switching, collapsed vs expanded width, and no antd-only props reaching the DOM. Full suite 155 tests, build and lint green. Worth noting for the remaining review: this is the class of defect the shim unit tests structurally cannot catch. They assert rendered output in jsdom, which has no layout engine — height 0 and height 212 look identical there. Only a real browser shows it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the previous Layout fix, which was only half right. `flex-auto` landed and the Sider collapse fix worked (the rail correctly renders at 65px now), but the dashboard was still empty: `.metrics-dashboard-container` remained 0px against production's 607px. The reason is the OTHER half of antd's Layout behaviour. A Layout containing a Sider lays out as a ROW; mine stayed a column, so the content area got no height. My first attempt inferred this from `React.Children`, which cannot work here: PageLayout renders `<SideNavBar>`, and the Sider lives *inside* that component. Compile-time child inspection can never see it. antd solves this with runtime context, so this does too — a Sider registers itself with the nearest ancestor Layout on mount, however deeply nested. The `__isSider` marker from the previous commit is gone; it was unreachable. Confirmed against production, whose outer Layout is `ant-layout ant-layout-has-sider` with `flex-direction: row` at 713px, versus mine at `flex-col` and 0px. The new test renders a Sider inside another component, matching how the real app does it — the earlier test passed a Sider as a direct child, which is exactly the case that already worked and why the bug survived. 156 tests, build and lint green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third layout defect found by comparing the dev deployment against production. On the workflows page the "Create Prompt Studio" dialog rendered at y=-109 with `transform: none` — pinned to the top of the viewport with its header clipped off-screen. Cause: shadcn's DialogContent is ALREADY centred, via `top-[50%] translate-y-[-50%]`. My Modal shim treated antd's `centered` prop as something it had to implement and appended `top-1/2 -translate-y-1/2` — the same geometry spelled differently. tailwind-merge sees two competing translate/top utilities, keeps one, and the dialog ends up with no transform at all. antd's `centered` is therefore a no-op here: the base component already does it. The prop is still destructured so it cannot land on the DOM as an invalid attribute, with a comment explaining why it is deliberately unused — otherwise this looks like an oversight and gets "fixed" back. Two regression tests: the base translate utilities must survive alongside `centered`, and the conflicting spelling must be absent. Audited the other shims for the same pattern (a wrapper adding positioning utilities on top of a shadcn primitive's own). The remaining `absolute`/`fixed` classes in antd-leaves and antd-structure are on elements those shims create themselves, so there is nothing to conflict with. 158 tests, build and lint green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourth defect found against the live deployment.
The Add LLM / Add Connector pickers render
`<List grid={{ gutter: 16, column: 4 }}>`. antd switches to an n-column grid
for that; my shim always rendered a divided vertical list, so every adapter
appeared one-per-row in a 600px scroller instead of 4-up. Measured in the
browser: `.list-of-srcs` children all sat at the same x with display:block.
The shim now honours `grid.column` (grid + grid-cols-n) and `grid.gutter`
(gap), and keeps the stacked divide-y list when no grid prop is passed. Column
classes are written out in a lookup rather than interpolated, since Tailwind
scans statically — same reasoning as the line-clamp table in antd-typography.
Two tests: grid mode applies grid-cols-4 and the gutter and drops divide-y;
non-grid mode still stacks.
160 tests, build and lint green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fifth defect found against the live deployment.
The Add LLM adapter settings form (RJSF) rendered 1109px tall in an 800px
viewport. The dialog was pushed to y=-194 and the Submit button sat off-screen,
so the form could be filled in but never saved.
antd wraps modal content in `.ant-modal-body`, and this app's CSS caps that
element — `.add-source-modal .ant-modal-body { height: 695px; overflow: hidden
auto }`, `.retrieval-strategy-modal .ant-modal-body { max-height: 70vh }` and
several more. My Modal shim rendered children directly into DialogContent, so
none of those rules matched anything and nothing constrained the height.
Content is now wrapped in a `.ant-modal-body` element. The class name is what
makes the existing per-modal CSS work again; the `max-h-[70vh] overflow-y-auto`
on it is the fallback for modals that never had a bespoke rule.
Found while verifying P3-05: the RJSF form itself is correct on @rjsf/core —
9 inputs, 3 required markers, descriptions, prefilled defaults, password reveal,
and Test Connection / Submit / Close all render. It was only unreachable.
161 tests, build and lint green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This is the systemic cause behind most of the layout defects found against the live deployment, rather than another one-off. The app has ~200 hand-written CSS rules that target antd's internal class names — `.ant-card-body`, `.ant-modal-content`, `.ant-tabs-nav`, `.ant-table-body`, `.ant-btn`, `.ant-typography` and ~65 more. antd emitted those elements; my shims did not, so every one of those rules silently matched nothing. 109 of them set layout properties (height, overflow, display, flex, padding), which is exactly why screens looked structurally right in the DOM and wrong on screen. Measured before and after: **109 dead layout rules across 53 classes → 8 across 8**. The 8 that remain are leaf styling on features this app does not currently render (card meta, textarea counters, tab overflow controls). The shims now emit the class names alongside their Tailwind classes. This is deliberate coupling to the legacy CSS, not an accident, and it is temporary: when that CSS is eventually rewritten against the design tokens, the hooks come out. The P1-05 layout shim already did this for `.ant-space-item`/`.ant-row`; this extends the same approach to the rest. Also fixed while here: Divider, Radio.Group/Radio, Segmented items, Popover inner, Result subtitle, Dropdown menu items and Collapse header/content were missing their hooks. Found by static audit rather than by opening screens — the previous five bugs were each discovered one page at a time, which does not scale and would have missed the ones on screens nobody happened to visit. 161 tests, build and lint green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…udio Most severe defect so far: opening any Prompt Studio project showed "Couldn't load this page" and rendered nothing. Cause: PromptCardItems.jsx and NotesCard.jsx render `<Collapse.Panel>`, and SetOrg.jsx renders `<Card.Meta>`. Neither sub-component existed on the shims, so React received `undefined` as an element type and threw error #130. That does not degrade one component — it takes down the entire route. Collapse now supports both antd forms: the `items` data prop and the legacy `<Collapse><Collapse.Panel header=…>` children, including `showArrow={false}` which PromptCardItems relies on. Card.Meta renders avatar/title/description. Added a completeness guard (shim-completeness.test.jsx) instead of only fixing the two. It scans the app source for every `<Foo.Bar>` usage and asserts the shims actually expose it. The per-component tests could not have caught this: nothing in them rendered Collapse.Panel, so its absence was invisible until a real page tried. The guard covers 14 sub-components today and fails loudly for any future gap. It earned its place immediately — it caught that my first Collapse.Panel assignment had not landed (biome had reordered the export block my patch anchored to, so the edit silently no-opped). 176 tests across 16 files, build and lint at the 24-warning baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by static audit rather than by clicking: scanning for `Foo.bar(...)`
calls on shim components turned up two undefined statics.
ConfirmModal calls `Modal.useModal()` and then `modal.confirm({...})`. Neither
existed, so every consumer threw a TypeError the moment its button was clicked.
That is 12 components — delete actions across prompt studio, workflows, manage
docs, LLM profiles, custom synonyms and the top nav.
useModal now returns `[api, contextHolder]` and implements confirm/info/
success/error/warning/destroyAll on AlertDialog, so it shares behaviour with
useConfirm() instead of becoming a second confirm pattern. Escape and
outside-click resolve as Cancel.
Modal.confirm is implemented too — the fully-imperative form callable outside
React, which mounts its own root. No OSS call-site uses it today, but the cloud
plugins have three.
Extended the completeness guard to cover static calls, not just `<Foo.Bar>`
JSX. It now strips comments before scanning: a doc comment mentioning
`Modal.confirm` is not a call-site, and flagging it would teach people to
ignore the test.
180 tests across 16 files, build and lint at the 24-warning baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleventh defect. The workflows "Create Prompt Studio" dialog rendered at
y=-127 with its header clipped off-screen, even after the earlier centring fix.
That earlier fix was correct — the classes were right this time. The override
came from the app's own stylesheet:
.prompt-studio-modal { padding: 10px; top: 20px; }
antd's modal wrapper is statically positioned, so `top: 20px` read as "20px
from the top of the viewport" and worked. The shadcn Dialog is
`position: fixed` and centres itself with `top: 50%` + `translateY(-50%)`, so
the same rule overrode the centring while the transform still applied — pulling
the dialog 127px above the viewport.
Removed the rule and left a comment explaining why, since it looks arbitrary
otherwise. Centring is the component's job now.
Added css-collisions.test.js rather than only fixing the one rule: it scans
every stylesheet for a modal/dialog ROOT selector setting top/bottom/transform
and fails with the offending file and rule. It deliberately ignores inner
elements (`__body`, descendant selectors, `.ant-*`), which cannot fight the
root's positioning. The remaining `.retrieval-strategy-modal__*` rules are
inner elements and are correctly not flagged.
This is the third distinct failure mode that jsdom cannot see (height 0, dead
CSS hooks, and now positional overrides), so it is worth having a static guard
rather than relying on someone opening the right screen.
182 tests across 17 files, build and lint at the 24-warning baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelfth defect, and the most silent one yet: Prompt Studio's Export button did nothing. No menu, no error, no network request — I instrumented fetch and XHR to confirm zero calls were made. Export is the child of a `<Dropdown>`, and Radix renders its trigger with `asChild`, attaching handlers through a ref. Neither CustomButton nor the base shadcn Button forwarded refs, so the ref went nowhere and the trigger was never wired up. A dropped ref throws nothing and logs nothing, which is why this survived 182 passing tests and a full route sweep — the page rendered fine, the button just wasn't connected to anything. Both now forward refs. That covers the 24 Dropdown call-sites, plus Popover and Tooltip triggers that use the same asChild mechanism. Audited the other primitives: Badge, Kbd, Label, Skeleton and Spinner are also plain functions, but none is used with asChild anywhere, so they are not causing breakage. Left alone rather than changed speculatively. Four regression tests: the base Button and CustomButton each forward to a real DOM node, a Dropdown wrapping CustomButton gets aria-haspopup/data-state (proving Radix wired the trigger), and the menu actually opens on click. 186 tests across 18 files, build and lint at the 24-warning baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by the shim-completeness guard once the enterprise plugins were overlaid: ReviewHeader.jsx:910 renders <Dropdown.Button>Download File</...>, and Dropdown.Button was undefined. That is React error #130, which takes down the whole manual-review route rather than just the button — the same failure mode as the Collapse.Panel bug. Dropdown.Button is NOT Dropdown. In <Dropdown> the child IS the trigger, so naively aliasing the two would make "Download File" open a menu instead of downloading. antd's split button keeps the halves separate: children is a real action button wired to onClick, and only the chevron opens the menu. The three new tests pin exactly that separation, since it is the one thing an alias would silently get wrong. The chevron half carries aria-label="More actions" so both halves stay distinguishable by accessible name.
The shim accepted `presets`, `disabledDate`, `allowClear`, `onOk` and
`format` and did nothing with them. Nothing crashed, so this survived the
migration invisibly — but three of the five are behaviour, not decoration:
- `presets` MetricsDashboard's "Last 7/30/90 Days" buttons never
rendered. Those are the primary way the range gets set,
so the control looked finished while its main affordance
was missing.
- `disabledDate` MetricsDashboard uses it to block future dates. Ignored,
users could query tomorrow. Now probed outward from today
and mapped onto the inputs' min/max, which is the bound a
native input can actually enforce.
- `allowClear` antd defaults to true; MetricsDashboard passes false
because its handler drops anything that is not a complete
pair. Emitting null there strands it on a stale range.
`onOk` now fires when a range becomes complete (there is no popup confirm
button to hang it off). `format` and `size` are destructured to keep them
off the DOM.
Also stops forcing moment on the way out. ExecutionLogs holds moment,
MetricsDashboard holds dayjs; the shim rebuilt every emitted date as moment,
handing MetricsDashboard a type it never opted into. It happens not to break
because that code only calls .toISOString(), which both implement — but it
quietly reverses D7's promise that this layer does not change what flows
through it. Emitted dates are now cloned from the caller's own instance.
Each of the five behaviours has a test, and each was mutation-checked: the
prop was re-broken one at a time and the matching test failed every time, so
these assert the fix rather than restating it.
… broken Live check on the dashboard caught this: the preset buttons rendered, but `disabledDate` produced no `max` bound, so future dates were still pickable — the very thing the previous commit claimed to fix. Cause: `new sample.constructor(isoish)` looks like a reasonable way to rebuild a date in the caller's library. It is wrong for both libraries in use. dayjs's internal constructor takes a config OBJECT, so handed a string it ignores it and returns TODAY. moment's returns an object that throws on .format(). So the disabledDate probe compared today against today on every iteration, never crossed the boundary, and yielded no bound. Now clones the caller's instance and re-points it field by field, which both libraries support (dayjs setters return a new instance, moment's mutate and return this; assigning the result covers both). The result is asserted to land on the exact requested instant before it is returned. The reason this got through: the test used a hand-written dayjs-shaped stub whose constructor DID accept a date string, so it validated the stub rather than the shim. Replaced with the real dayjs and moment, plus a case pinning the actual predicate MetricsDashboard passes. Re-broken deliberately to confirm the new test fails against the old approach.
The shim consumed five antd Select props without implementing any of them,
and each one was load-bearing at a call-site:
- `dropdownRender`/`popupRender` pins a "create one" action under the option
list. Configure Connector's "+ Add new connector" and the Lookup drawer's
"Create Lookup" are both rendered that way, and both are the ONLY route to
creating one — an org with no connectors got an empty dropdown and no way
out.
- `labelInValue` makes antd read and write the selection as `{ value, label }`.
Configure Connector is written against it, so a bare string arrived instead,
`option?.value` came back undefined, and picking a connector did nothing.
- `showSearch` (27 call-sites) filters the list as you type.
- `filterOption` / `optionFilterProp` / `notFoundContent` go with it.
`showSearch` cannot be Radix's Select: it moves DOM focus onto the highlighted
option, runs a typeahead on the content, and re-focuses items on pointermove,
so a nested input loses both its keystrokes and its caret. Those selects now
render as a Popover-anchored ARIA combobox instead; the rest are untouched.
The trigger/content/item class strings move into select.tsx so the two
variants cannot drift apart.
Options are also no longer normalised into `{ value, label }`. antd passes
filterOption the option's own props, and the call-sites read whatever field
they authored — `option.children` (Summarize Manager), a string `option.label`
set beside a rich `children` node (Adapter Selection), `option.data.label`
(Configure Connector). Under the old shape the first two were undefined and a
React element respectively, and both throw on `.toLowerCase()` — a crash on
the first keystroke, once search started working.
`biome ci` flagged 10 unformatted files and one unsorted import block, which is what turned the frontend lint check red. Pure formatting — run of `biome check --write`, no behaviour change.
Two separate reasons pre-commit.ci was red or blind: check-json parses with strict `json.loads`, so it choked on the comments in frontend/tsconfig.json. tsc reads that file as JSONC and the comments explain non-obvious compiler settings, so exempt the file rather than strip them — same treatment .vscode/launch.json already had. biome-check was skipped in CI entirely because `language: system` meant it depended on a local bun. Switching to `language: node` lets pre-commit provision Biome itself, so the hook now runs there too. Not the upstream biomejs/pre-commit hook: it runs from the repo root, but Biome's project root here is frontend/, where biome.json lives. From the root, Biome resolves the wrong package.json — measured on this tree, that regroups organizeImports and reformats three CSS files, so the hook and the GitHub workflow would rewrite each other's output forever.
Sonar flagged the io() call as a streaming connection whose target comes from a user-influenced value: getBaseUrl() reads window.location. The argument was redundant. With no URL, socket.io builds `loc.protocol + "//" + loc.host` from window.location itself — the same origin getBaseUrl() returned — so same-origin behaviour is unchanged and the value no longer flows through application code.
Both functions open with a null/undefined guard, but the JSDoc promised
`{number}`. Sonar believed the annotation and reported the guards as
comparisons that can never be true — a reliability drop on new code for
what is really a stale type. The guards are right; the annotation was
too narrow.
The manifest asked for `@playwright/test: ^1.49.0` with no lock file, so every install could resolve a different browser driver — Sonar reports the unpinned range as a vulnerability. Lock it with bun, matching frontend/bun.lock. The group stays `optional: true` in tests/groups.yaml and still skips without node_modules, so this changes nothing about what CI runs.
Sonar's last three reliability bugs, all 'visible non-interactive element with a click handler and no keyboard listener'. Two are passive wrappers that exist only to catch clicks bubbling out of a dropdownRender footer. They carry no semantics, so mark them `role=presentation` — which in the Radix path also stops the wrapper breaking the listbox/option relationship it sits inside. The third is the searchable select's `role=option`. That one is a false positive: it's the aria-activedescendant pattern, so options are never focusable and the input's onKeyDown drives Arrow/Enter into the same `choose` the click calls. A key handler on the option could never fire, so suppress rather than add dead code.
The Playwright baseURL fell back to UNSTRACT_BACKEND_URL, and the rig exports that for every group with requires_platform. Nothing ever set UNSTRACT_FRONTEND_URL, so the chain always resolved to the backend — compose publishes the frontend on :3000 and the backend on :8000, so the specs would have driven the API instead of the UI, and the correct :3000 default was unreachable in exactly the case it existed for. Latent so far only because the group is `optional` and skips without node_modules. Fix it at the source: carry frontend_url on PlatformEndpoints like every other service URL and export it from _execute_group, then drop the backend fallback from the config — the backend origin is never a valid frontend origin, so falling back to it can only ever be wrong. Reported by Greptile on the PR.
Open any dialog with the Logs footer visible and the footer stayed bright white over the dimmed page, and stayed clickable through the overlay. The shadcn primitives kept Radix's stock `z-50` while this app predates that scale and still parks chrome in the hundreds and thousands — `.logs-container` is 999, the agency canvas and settings rail are 1000 — so everything in that band punched through the mask. Under antd the mask was z-index 1000 and covered them, which is why this only shows up now. Raise dialog/alert-dialog/sheet (overlay and content) to 1100: clear of the legacy band, still below `[data-radix-popper-content-wrapper]` (1500) so a Select opened inside a modal keeps rendering above it, and below `.fullscreen-loader` (2000). The full ladder is now written down next to the popper override in index.css. While in the Logs panel, move its hardcoded #fff/#ccc/#fff1f0 onto --card/--border and a --destructive mix. Light mode is unchanged in value — --card is white — but the panel no longer stays a white slab in dark.
Five shims swallowed the attribute outright. Select, Popover and Popconfirm
spread `...props` onto a Radix *Root*, which renders no DOM at all; Dropdown
and Tabs destructured `...props` and then never used it. In every case the id
vanished with no warning — the same silent prop-drop this layer keeps
producing, and indistinguishable at a call-site from a typo in the id.
That made every test id written against those components dead weight, and the
failure only showed up in Playwright, against a running stack, as a locator
matching nothing.
Where the id lands is chosen per component, not incidental:
trigger Select — what a test clicks, and the only element the shim itself
renders. All three modes are wired, because `showSearch` and
`tags` render entirely different widgets via early exits.
content Modal, Dropdown, Popover, Popconfirm — portalled out of the tree,
so they have no stable position and only library classes to select
on. Their triggers are `children`, which the call-site renders and
can label itself. This is also where each shim already sends `ref`.
Elements the shim builds from a data descriptor — menu items, tab triggers,
segments, and the Modal/Popconfirm footer buttons — cannot be labelled from
the call-site at all, so they derive an id from the parent's. Deriving is
opt-in: with no parent id nothing is emitted, so the app does not silently
sprout generic `tab-1` ids that collide between two tab strips on one page.
`"data-testid"?: string` has to be declared explicitly on each props
interface: React's HTMLAttributes does not carry `data-*` (JSX lets them
through on intrinsic elements only), so destructuring one is a type error
without it. All additions are optional, so the cloud plugin tree still
compiles against these interfaces.
testid-forwarding.test.jsx guards the whole surface, including that deriving
stays opt-in and that an explicit id on an item beats the derived one.
Sweeps the surfaces the e2e suite drives — the listings, their rows, the
create/confirm modals, the sidebar fly-outs and the Prompt Studio authoring
header — for controls a test cannot reliably reach today.
The rule applied throughout: skip anything with a unique id, aria-label,
placeholder or static non-repeating text; add wherever the only handle is a
library class, a portal, a repeated row, or text driven by data.
The highest-leverage cases were the shared widgets, where one fix covers
every screen that renders them:
ResourceTable Prompt Studio, Workflows, adapter Settings and Connectors
all use it, and every row's actions shared one aria-label
("Edit Prompt Project"). Rows and controls are now keyed by
the record id via a page-specific `testIdPrefix`, so a
locator reads as the screen rather than as the widget.
CardActionBox Four icon-only buttons with no text and no aria-label,
repeated once per card — nothing on that row was selectable.
Note the id shape: a row is `<prefix>-row-<id>` and a control inside it is
`<prefix>-<action>-<id>`, deliberately NOT `<prefix>-row-<action>-<id>`. The
latter reads better but makes every action button a prefix-match for its own
row, leaving `[data-testid^="…-row-"]` matching five elements instead of one.
Skipped, so reviewers can check the reasoning: fields inside `Form.Item` (the
shim already wires `id={name}` and points `<Label htmlFor>` at it), and
buttons whose text is static and unique on the page ("Deploy as API", "Manage
Documents", "New Project"). Those last ones are an i18n risk the moment the
app is localised.
Segmented is a known gap: each option is now selectable, but WHICH is active
is still expressed only by Tailwind classes. Exposing that means adding a
state attribute rather than a test id, so it is left for a follow-up. Radix
supplies `data-state` on tab triggers natively, so Tabs need nothing.
Three of these specs had been silently SKIPPING, not passing. They located
rows with `.list-view-row` (and `.ant-list-item`), neither of which exists
any more — the first died when the listings moved to ResourceTable, the
second with the antd removal. Each is guarded by `if (count === 0)
test.skip(...)`, so a selector matching nothing reported "skipped" and the
suite stayed green:
prompt studio › an existing project opens on the Document Parser
workflows › a workflow opens and exposes its actions
agentic prompt studio › (three of four tests)
`.settings-sidebar-popover` had a different problem: the HITL fly-out renders
the same class, so the locator was ambiguous whenever both were present.
Replaced, and verified against a running app rather than assumed:
.list-view-row -> [data-testid^="prompt-studio-list-row-"]
[data-testid^="workflow-list-row-"]
[data-testid^="aps-project-list-row-"]
.settings-sidebar-popover -> platform-menu + platform-menu-item-users
img, [class*='card'] -> [data-testid^="ds-card-"]
[role=switch] .first() -> scoped inside api-deployment-list-card-*
dialog OK buttons by label -> *-modal-ok (labels flip Save/Update,
Create Workflow/Edit Workflow)
New LLM Profile by name -> llm-adapter-add-btn (the label is per-route
copy, keyed off the route's `type`)
Scoping the API-deployment assertions inside a single card replaces a
page-wide `[role=switch]` + `.first()`, which would happily have matched a
switch belonging to a different card.
Role- and text-based locators are deliberately left alone where the target
has a unique accessible name — getByRole("button", { name: /New Project/i }),
the agentic tab assertions, getByText(/Manage Users/i). Those are stable
queries, not framework-class guesses.
Two more antd props the shims never consumed, both caught by reading the
browser console against a running app rather than by the suite — a React
warning fails no test.
`bordered` reached DataTable's wrapper <div>, where React rejects it outright
("Received `true` for a non-boolean attribute `bordered`") on every render.
It is the fourth prop to land there after onRow, showHeader and scroll, so it
is declared alongside them — and honoured rather than merely swallowed: antd
draws rules between cells and an outer frame, and the agentic Prompt Studio's
document-status and extracted-data tables ask for it, so they had been
rendering borderless all along.
The cell rules are applied with a descendant selector rather than per-cell
classes, so they also cover cells a caller renders itself via
`columns[].render`.
`mouseEnterDelay` reached the tooltip TRIGGER. That path is not accidental:
`rest` is deliberately forwarded to the trigger so Radix's own aria/data
attributes reach the anchored element, which means anything undeclared rides
along to the DOM with them. It is now mapped to Radix's `delayDuration`,
scaling antd's seconds to Radix's milliseconds, and only when the call-site
actually passes one — so every existing tooltip keeps Radix's default timing
instead of silently changing.
`mouseLeaveDelay` is consumed but NOT honoured, and says so: Radix's Tooltip
has no close-delay prop, and `disableHoverableContent` is a different
behaviour. Declaring it keeps it off the DOM either way.
Both are guarded in the style the DataTable suite already uses for `scroll`
("does not leak … onto the DOM as an attribute"). Verified in the browser:
the two React warnings are gone, no element carries either attribute, and the
bordered table now actually draws its borders.
| * URL — so accepting it here would silently point every spec at the API and | ||
| * make the :3000 default unreachable in exactly the case it exists for. | ||
| */ | ||
| const baseURL = process.env.UNSTRACT_FRONTEND_URL ?? "http://localhost:3000"; |
There was a problem hiding this comment.
When the rig runs the ui group against its default Compose stack, Playwright navigates to localhost:3000, where the production frontend nginx serves index.html for relative /api/v1/* requests instead of proxying them to the backend. Session initialization therefore fails and authenticated specs reach the login or error state rather than exercising the migrated screens, while the document-only smoke test can still pass.
Knowledge Base Used:
… searched
These lists are all fetched at runtime and grow with the org: the LLM
profile form's four adapter pickers, the workflow picker on the API
deployment and ETL/Task create modals, and the prompt card's enforce-type
list. Scrolling them was the only way to find an entry.
The Select shim already implements `showSearch` end to end, so each of
these is the prop and nothing else -- except enforce type.
Enforce type also drops `optionFilterProp="children"`. That list is built
as `{ value: "text" }` with no label and no children (DocumentParser), so
the filter read an absent field, matched the empty string against every
query and rendered "No results" on the first keystroke. antd behaves the
same way, so this was a latent call-site bug that only became reachable
once the box existed. Without the prop the filter falls back to the
option's own text, which is the value the trigger already shows.
Two shim rules that let content escape horizontally.
Layout had `min-h-0` but no `min-w-0`. A Layout is itself a flex item, and a
flex item's automatic minimum size is its CONTENT's max-content width, so a
page wide enough to overflow pushes the Layout past its track instead of
scrolling inside it. The agentic Prompt Studio nests percentage-width panes
(`.pd-left-panel { width: 50% }`) around a table the DataTable gives
`min-width: max-content` (antd `scroll={{ x: true }}`), and the two resolve
against each other until the shell measures ~500,000px wide: 1560px window,
Layout row 500,945px. Everything right of the viewport was rendered and
off-screen — the Export button, the whole PDF pane, five of the six status
columns. It read as "half the page is missing", not as an overflow.
Segmented items lacked `whitespace-nowrap`, which antd segments have. A
two-word label broke mid-control the moment the segmented sat in a tight row:
the same document pane rendered "Raw Text" as "Raw" over "Text".
`locale={{ emptyText }}` is the spelling six call-sites actually use, and it
was undeclared — the same class of gap already fixed for `onRow`,
`showHeader`, `scroll` and `bordered`. It failed both ways at once: the
custom empty state was silently replaced by the bare "No data" default, and
the object landed on the wrapper <div> as `locale="[object Object]"`.
The agentic Prompt Studio's status table is the visible casualty. A project
with no documents showed an empty box where "No documents in this project
yet — click Manage Documents on the right panel to upload PDFs" should be,
which reads as a broken table rather than an empty one.
`emptyText` still works; `locale.emptyText` takes precedence when both are
given, as antd does.
A toast raised by an open modal could not be dismissed. Radix sets
`pointer-events: none` on <body> for a modal Dialog, and sonner's viewport
is an ordinary body-level element, so it inherits that and stops taking
clicks — the toast still paints above the overlay (z-index 999999999 vs
z-[1100]), so it looks live but its close button does nothing. Hit on
Workflows: "New Workflow" with a duplicate name toasts the backend error
and the toast then cannot be cleared.
Re-enable pointer events on `[data-sonner-toaster]`, the same way
`.notification-clear-all` already does. That alone is not enough: Radix
would read the regained clicks as an interaction outside the dialog and
close it, taking the half-typed form with it. NewWorkflow passes
`maskClosable={false}`, but the shim honours that only on
`onPointerDownOutside` — the focus path would still dismiss. So wrap the
toast stack in a DismissableLayer.Branch, which is Radix's own mechanism
for UI that lives outside a layer but must not dismiss it, and covers both
paths for every layer type rather than just dialogs.
…teps Selecting any FILESYSTEM connector in a workflow threw React #130 and took down the whole page with "Couldn't load this page". FileSystem.jsx lifts `const { DirectoryTree } = Tree` at module scope, but the Tree shim was a plain forwardRef with no DirectoryTree static, so the file browser rendered `undefined` as an element type. DirectoryTree cannot reuse TreeBase: that one renders every node expanded and has no notion of loadData, but the connector browser lists one directory at a time and folders come back childless. So it gets its own implementation with controlled/uncontrolled expandedKeys and selectedKeys, lazy loadData on first expand, and expandAction={false} — which is what lets you pick a destination folder without also opening it. Steps had the same hole one level down: no Step static, so the Prompt Studio "Deploy as API" wizard was a second #130. Beyond that the shim ignored onChange and per-item status, which left the cloud onboarding stepper inert — every step rendered as text, so the guide could not be used to navigate and completed steps looked identical to the current one. Both now behave as antd does, along with disabled and description, through `items` and the legacy <Steps.Step> children form alike.
shim-completeness exists to catch exactly the break that just shipped, and it
passed the whole way. Two reasons, both of which made its coverage smaller
than it looked:
Its SHIMS map was hand-maintained, and a parent missing from it is skipped
silently rather than failing. Tree and Steps were both absent. It is now
built from every shim module's exports, so adding a shim component no longer
also requires remembering to register it here.
The scan only read `<Foo.Bar>` and `Foo.bar(` out of the source, never
`const { Bar } = Foo` — which is the form antd's own docs use and the one
both broken call-sites use. Tabs.TabPane only looked covered because it
happens to appear inline somewhere else.
With both fixed the guard reports Tree.DirectoryTree and Steps.Step, and
fails on the code as it was before the previous commit.
The shim's tags-mode branch dropped `options` on the floor and drew no border, so Configure Connector's "File types to process" rendered as bare text with nothing to pick from — and its ArrayField silently discards anything typed that the enum does not contain, so the field could not be filled in at all. TagsInput now anchors a Popover listbox when options are supplied (filtered by the draft, chosen values removed, arrows + Enter, Escape closing the popup without the surrounding modal), honours `maxCount` for the prompt card's two "pick one" fields, and wears the same border as the Select trigger. Free-text-only call-sites are unchanged.
Three shim gaps, all found from one report that the HITL "Select Document
from Queue" dialog was broken after the migration.
- Spin only handled the bare indicator, and its comment said so. Three
plugins use the WRAPPER form, `<Spin spinning={loading}>{content}</Spin>`;
because the old body destructured `{size, tip, className, ...props}` and
then supplied its own JSX children, `props.children` was discarded
outright. FetchSpecificModal rendered a permanently spinning dialog with
no document list and no empty state, and `spinning` leaked onto the DOM
as an unknown attribute. The wrapper now dims and freezes its children
under an overlay rather than unmounting them, which is what antd does and
what keeps scroll and focus across a reload. Class names follow antd's
real structure — ant-spin-nested-loading outside, ant-spin-container on
the dimmed child; the old code put ant-spin-container on the bare
indicator, which no stylesheet relied on.
- Tag never sized its `icon`. antd's icons were a font and inherited
font-size; lucide ships SVGs carrying width/height 24, and the
inline-icon rule in index.css cannot reach them because Badge is
inline-flex and that rule deliberately skips flex parents. The HITL
reviewer chip drew a 24px glyph beside 12px text.
- Input.Search does not delegate to InputBase, so it never consumed
`allowClear` and React warned on every render of the queue search box.
It now draws the clear affordance too, emitting the same
ChangeEventLike shape the other controlled shims in this file use.
Six regression tests cover the dropped children (both spinning states),
the leaked attribute, the icon size, and the clear button.
Verified in a browser against a dev namespace, not just in jsdom: the list
and empty state render, and a MutationObserver across a search confirms the
container goes aria-busy with the spinner overlaid and the rows still
mounted, then settles back.
for more information, see https://pre-commit.ci
PdfViewer builds a `defaultLayoutPlugin` but only ever imported the highlight stylesheet, so `.rpv-default-layout__*` had no rules of its own. It looked right only when some other module that does import them — DocumentManager, or the app-deployment / llm-whisperer / agentic viewers — happened to be in the loaded module graph. Land straight on the HITL review page and none of them are: the toolbar renders unstyled and the sidebar stacks above the page. Core is imported for the same reason. It was arriving via the optional plugins/pdf-highlight/RenderHighlights dynamic import, which is just as incidental and is absent in builds without that plugin.
Import Project sets `beforeUpload: () => false` because it submits the file itself. In antd that cancels only the UPLOAD — the file still lands in fileList and onChange still fires. The shim treated `false` as "drop it" and returned early, so the modal's fileList stayed permanently empty: nothing appeared after picking a file, and Import answered "Please select a file to import" for the JSON the user had just chosen. The adapter selection step was unreachable. The existing test only asserted beforeUpload was called, never that onChange followed, which is how this got past the shim guard. Also in the shim, all of a piece with that contract: - `Upload.LIST_IGNORE` was undefined, so a call-site returning it did not match `=== false` and fell through to the SUCCESS path — Look-up Studio's oversize-file veto reported a file it had just rejected as uploaded. - `showUploadList` (on by default in antd), `onRemove` and `maxCount` were spread onto the DOM and ignored. Without the list there is no on-screen feedback at all in the beforeUpload-false flow. ImportTool takes `maxCount: 1` so a second pick replaces the first, and resets when the modal closes — the parent closes it directly after a successful import, so the next open carried the previous file. Verified end to end against a dev namespace: pick JSON -> file listed -> Import -> adapter modal -> project created.
Every control in the prompt-card toolbar, the card header and the per-LLM
profile column is an icon-only button whose only distinguishing class comes
from the shim (ant-btn, prompt-card-action-button), and the last three groups
repeat once per prompt and once per profile. Both are ADD cases under the
decision order in FRONTEND_DEV_GUIDE.md, and none of them were selectable
without depending on position.
Ids follow {context}-{element}-{action} with the record id LAST, so a
repeated control is ps-prompt-run-doc-<promptId> rather than
ps-prompt-<promptId>-run. The profile column needs both ids because a control
there repeats along two axes.
ExpandCardBtn and PromptOutputExpandBtn render their own button, so they take
the id as a prop. AntRadioProps declares data-testid for the same reason the
other shims do: an undeclared prop is silently swallowed by ...props, which is
the failure this layer keeps producing.
The run buttons in the toolbar are also renamed one-doc/all-docs so neither id
is a prefix of the other.
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
Unstract test resultsPer-group results
Critical paths
|




Implements P1-01 … P1-04 of
UN_SHADCN_IMPL_PLAN.md.Contains the P0 foundation commit too (
a0473a1) — see #2211 for that in isolation. Review commit-by-commit:a0473a126a8c64612875ba0f3848The main decision to review: shims instead of find-and-replace
The plan specified mapping antd props to Tailwind classes inline. That is unsafe for Typography and Button, because both carry behavior, not just styling — and silently dropping behavior is a C4 violation, not a restyle:
Typography ellipsis—ellipsis={{ tooltip: true }}truncates and surfaces full text on hover;{{ rows: 2 }}clamps lines. 12 call-sites use the object form. Tailwind'struncateis CSS-only, so a class swap would delete the tooltip.Button loading(234 usages) — swaps in a spinner and disables the button. Dropping the disable would permit double-submits on in-flight requests.Button danger— orthogonal totype, so not a 1:1 variant map (danger+text must stay ghost-with-destructive-text).So each got a small shim presenting antd's API on shadcn primitives + Midnight Bloom tokens. The 295 Typography and 70 Button call-sites then become import swaps with JSX untouched — same elements, same order, same props, which is exactly what C4 asks for. Per D9/§5.0 both live in OSS so cloud plugins import them in Phase C.
CustomButton(76 usages) is a pass-through over antd's Button, so it routes through the shim automatically.Two subtle bugs caught
FileUpload.jsx/FileWidget.jsximport antd'sUploadcomponent, shadowed by the lucideUploadicon — would have broken both upload widgets.Workflows.jsxdefines its ownUsercomponent; importing lucide'sUsermade it render itself (infinite recursion).useRetrievalStrategies.jsemits icon name strings consumed by a modal'sICON_MAP; the modal's keys became lucide names while the hook still emitted antd names, so every lookup would have silently fallen back to a default icon.Verification
main--radius-mdtoken.Plan estimates are running low — worth correcting
The original enumeration used single-line regexes that missed multi-line import blocks. I re-measured all remaining components; accurate per-component counts are in the commit history.
🤖 Generated with Claude Code